import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:player_plugin/polyv_player.dart';
import 'package:player_plugin/video_player_value.dart';


class PolyvController extends ValueNotifier<VideoPlayerValue> {
  late MethodChannel _channel;

  final String dataSource;
  final DataSourceType sourceType;
  final bool allowBackgroundPlayback;

  Timer? _timer;
  bool _isDisposed = false;
  Completer<void>? _creatingCompleter;
  StreamSubscription<dynamic>? _eventSubscription;
  _VideoAppLifeCycleObserver? _lifeCycleObserver;

  PolyvController({
    required this.dataSource,
    required this.sourceType,
    this.allowBackgroundPlayback = false,
  }) : super(const VideoPlayerValue(duration: Duration.zero));

  /// Attempts to open the given [dataSource] and load metadata about the video.
  Future<void> initialize({required int viewID}) async {
    if (!allowBackgroundPlayback) {
      _lifeCycleObserver = _VideoAppLifeCycleObserver(this);
    }
    _lifeCycleObserver?.initialize();
    _creatingCompleter = Completer<void>();
    _channel = MethodChannel('polyv_video_plugin_$viewID');
    switch (sourceType) {
      case DataSourceType.videoVid:
        _setVid(dataSource);
        break;
      case DataSourceType.videoUrl:
        _setURL(dataSource);
        break;
      case DataSourceType.videoPath:
        _setVideoPath(dataSource);
        break;
    }
    _creatingCompleter!.complete(null);
    final Completer<void> initializingCompleter = Completer<void>();
    _channel.setMethodCallHandler((MethodCall call) async {
      switch (call.method) {
        case 'initialized':
          Duration duration = await getDuration();
          int volume = await getVolume();
          value = value.copyWith(
            duration: duration,
            volume: volume / 100,
            isInitialized: true,
            errorDescription: null,
            isCompleted: false,
          );
          initializingCompleter.complete(null);
          _applyLooping();
          _applyVolume();
          _applyPlayPause();
          break;
        case 'completed':
          // In this case we need to stop _timer, set isPlaying=false, and
          // position=value.duration. Instead of setting the values directly,
          // we use pause() and seekTo() to ensure the platform stops playing
          // and seeks to the last frame of the video.
          pause().then((void pauseResult) => seekTo(value.duration));
          value = value.copyWith(isCompleted: true);
          break;
        case 'bufferingUpdate':
          int bufferPercentage = call.arguments['bufferPercentage'];
          bool isBuffering = call.arguments['isBuffering'];
          value = value.copyWith(
              isBuffering: isBuffering, bufferPercentage: bufferPercentage);
          break;
        case 'videoSize':
          int width = call.arguments['width'];
          int height = call.arguments['height'];
          value =
              value.copyWith(size: Size(width.toDouble(), height.toDouble()));
          break;
        case 'videoPlayError':
          pause();
          String reason = call.arguments['reason'];
          value = value.copyWith(
            errorDescription: reason,
            isPlaying: false,
            isInitialized: false,
          );
          break;
      }
    });

    return initializingCompleter.future;
  }

  @override
  Future<void> dispose() async {
    if (_isDisposed) {
      return;
    }

    if (_creatingCompleter != null) {
      await _creatingCompleter!.future;
      if (!_isDisposed) {
        _isDisposed = true;
        _timer?.cancel();
        await _eventSubscription?.cancel();
      }
      _lifeCycleObserver?.dispose();
    }
    _isDisposed = true;
    super.dispose();
  }

  ///设置视频id
  Future<void> _setVid(String vid) async {
    return _channel.invokeMethod('setVid', {"vid": vid});
  }

  ///设置视频url
  Future<void> _setURL(String url) async {
    return _channel.invokeMethod('setVideoURI', {"url": url});
  }

  Future<void> _setVideoPath(String path) async {
    return _channel.invokeMethod('setVideoPath', {"path": path});
  }

  ///暂停
  Future<void> pause() async {
    value = value.copyWith(isPlaying: false);
    await _applyPlayPause();
  }

  ///播放
  Future<void> play() async {
    if (value.position == value.duration) {
      await seekTo(Duration.zero);
    }
    value = value.copyWith(isPlaying: true);
    await _applyPlayPause();
  }

  ///循环播放
  Future<void> setLooping(bool looping) async {
    value = value.copyWith(isLooping: looping);
    return _applyLooping();
  }

  Future<void> _applyLooping() async {
    if (_isDisposedOrNotInitialized) {
      return;
    }
    await _channel.invokeMethod('enableLoop', {"loop": value.isLooping});
  }

  /// The position in the current video.
  Future<Duration?> get position async {
    if (_isDisposed) {
      return null;
    }
    int currentPosition = await _channel.invokeMethod('currentPosition');
    return Duration(milliseconds: currentPosition);
  }

  Future<void> _applyVolume() async {
    if (_isDisposedOrNotInitialized) {
      return;
    }
    double value = this.value.volume * 100;
    int volume = value.toInt();
    await _channel.invokeMethod('setVolume', {"volume": volume});
  }

  Future<void> _applyPlayPause() async {
    if (_isDisposedOrNotInitialized) {
      return;
    }
    if (value.isPlaying) {
      await _channel.invokeMethod('play');

      // Cancel previous timer.
      _timer?.cancel();
      _timer = Timer.periodic(
        const Duration(milliseconds: 500),
        (Timer timer) async {
          if (_isDisposed) {
            return;
          }
          final Duration? newPosition = await position;
          if (newPosition == null) {
            return;
          }
          _updatePosition(newPosition);
        },
      );

      // This ensures that the correct playback speed is always applied when
      // playing back. This is necessary because we do not set playback speed
      // when paused.
      await _applyPlaybackSpeed();
    } else {
      _timer?.cancel();
      await _channel.invokeMethod('pause');
    }
  }

  Future<void> _applyPlaybackSpeed() async {
    if (_isDisposedOrNotInitialized) {
      return;
    }

    await _channel
        .invokeMethod('setPlaybackSpeed', {"speed": value.playbackSpeed});
  }

  void _updatePosition(Duration position) {
    value = value.copyWith(
      position: position,
      isCompleted: position == value.duration,
    );
  }

  ///是否播放中
  Future<bool> isPlaying() async {
    return await _channel.invokeMethod('isPlaying');
  }

  ///是否缓冲中
  Future<bool> isBuffering() async {
    return await _channel.invokeMethod('isBuffering');
  }

  ///时长
  Future<Duration> getDuration() async {
    int millisecond = await _channel.invokeMethod('duration');
    Duration duration = Duration(milliseconds: millisecond);
    return duration;
  }

  ///当前节点
  Future<int> getCurrentPosition() async {
    int currentPosition = await _channel.invokeMethod('currentPosition');
    return currentPosition;
  }

  ///跳转某节点
  Future<void> seekTo(Duration position) async {
    if (_isDisposedOrNotInitialized) {
      return;
    }
    debugPrint("seekTo: $position ${value.duration}");
    if (position > value.duration) {
      position = value.duration;
    } else if (position < Duration.zero) {
      position = Duration.zero;
    }
    int milliseconds = position.inMilliseconds;
    await _channel.invokeMethod('seekTo', {"progress": milliseconds});
    _updatePosition(position);
  }

  ///缓冲百分比
  Future<int> getBufferPercentage() async {
    return await _channel.invokeMethod('bufferPercentage');
  }

  /// 获取亮度
  Future<double> getBrightness() async {
    int tempBrightness = await _channel.invokeMethod('brightness');
    return tempBrightness / 100;
  }

  ///音量
  Future<int> getVolume() async {
    return await _channel.invokeMethod('volume');
  }

  ///设置亮度
  Future<void> setBrightness(double brightness) {
    double tempBrightness = brightness * 100;
    debugPrint("brightness: $tempBrightness");
    return _channel
        .invokeMethod('setBrightness', {"brightness": tempBrightness.toInt()});
  }

  ///设置音量
  Future<void> setVolume(double volume) async {
    value = value.copyWith(volume: volume.clamp(0.0, 1.0));
    await _applyVolume();
  }

  /// 获取播放速度
  Future<double> playRate() async {
    return await _channel.invokeMethod('playbackSpeed');
  }

  /// 设置播放速度
  Future<void> setPlaybackSpeed(double speed) async {
    if (speed < 0) {
      throw ArgumentError.value(
        speed,
        'Negative playback speeds are generally unsupported.',
      );
    } else if (speed == 0) {
      throw ArgumentError.value(
        speed,
        'Zero playback speed is generally unsupported. Consider using [pause].',
      );
    }

    value = value.copyWith(playbackSpeed: speed);
    await _applyPlaybackSpeed();
  }

  @override
  void removeListener(VoidCallback listener) {
    // Prevent VideoPlayer from causing an exception to be thrown when attempting to
    // remove its own listener after the controller has already been disposed.
    if (!_isDisposed) {
      super.removeListener(listener);
    }
  }

  bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized;
}

/// This allows a value of type T or T? to be treated as a value of type T?.
///
/// We use this so that APIs that have become non-nullable can still be used
/// with `!` and `?` on the stable branch.
T? _ambiguate<T>(T? value) => value;

class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver {
  _VideoAppLifeCycleObserver(this._controller);

  bool _wasPlayingBeforePause = false;
  final PolyvController _controller;

  void initialize() {
    _ambiguate(WidgetsBinding.instance)!.addObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      _wasPlayingBeforePause = _controller.value.isPlaying;
      _controller.pause();
    } else if (state == AppLifecycleState.resumed) {
      if (_wasPlayingBeforePause) {
        _controller.play();
      }
    }
  }

  void dispose() {
    _ambiguate(WidgetsBinding.instance)!.removeObserver(this);
  }
}
